/**
 * @Name:		utils.js
 * @Object:		pc/mobile
 * @Author:		Xiaoyu/Guo CJ
 * @E-mail:		592429285@qq.com
 * @Time:		2023.06.07
 */

class UTILS {
    #win = window;
    #doc = document;
    #docDm = this.#doc.documentElement;
    #docBody = this.#doc.body;
    #viewportWidth = this.#win.innerWidth || this.#docDm.clientWidth || this.#docBody.clientWidth;
    #viewportHeight = this.#win.innerHeight || this.#docDm.clientHeight || this.#docBody.clientHeight;
    #scrollTop = this.#win.pageYOffset || this.#docDm.scrollTop || this.#docBody.scrollTop;
    #scrollLeft = this.#win.pageXOffset || this.#docDm.scrollLeft || this.#docBody.scrollLeft;
    // 默认倒计时时间规格
    #countdown = 60;
	#loginCountdownTime = 1800;
	#setTimeoutTimer = 0;

    // 视口宽度
    get VIEWPORT_WIDTH() {
        return this.#viewportWidth;
    }

    // 视口高度
    get VIEWPORT_HEIGHT() {
        return this.#viewportHeight;
    }

    // 纵向滚动条已滚动的距离（高度）
    get SCROLL_TOP() {
        return this.#scrollTop;
    }

    // 横向滚动条已滚动的距离（宽度）
    get SCROLL_LEFT() {
        return this.#scrollLeft;
    }

    /**
     * setPageWidthMB - 移动端端初始化默认设置 html 根元素的字号的尺寸
	 * (移动端以 750px 的页面宽度为标准的 1rem 的大小)
     * 页面结构模板详见 layout.css
     */
    setPageWidthMB() {
		const doc =  this.#doc;
        const docEl = this.#docDm;
        // 设备像素比 devicePixelRatio, dpr = 物理像素/独立设备像素
        const dpr = window.devicePixelRatio || 1;

        // adjust body font size
        function setBodyFontSize () {
            const docBody = doc.body;
            docBody && (docBody.style.fontSize = `${(12 * dpr)}px`);
        }
        setBodyFontSize();

        // set 1rem = viewWidth / 10
        function setRemUnit () {
            docEl.style.fontSize = `${docEl.clientWidth / 10}px`;
        }
        setRemUnit();

        // reset rem unit on page resize
        window.addEventListener('resize', setRemUnit);
        window.addEventListener('pageshow', e => e.persisted && setRemUnit());

        // detect 0.5px supports
        if(dpr >= 2) {
            const fakeBody = doc.createElement('body');
            const creDivDom = doc.createElement('div');
            creDivDom.style.border = '.5px solid transparent';
            fakeBody.appendChild(creDivDom);
            docEl.appendChild(fakeBody);
            (creDivDom.offsetHeight === 1) && docEl.classList.add('hairlines');
            docEl.removeChild(fakeBody);
        }
    }

    /**
     * setPageWidth - PC端初始化默认设置 html 根元素的字号的尺寸
	 * (pc兼容移动设备显示)
     * 页面结构模板详见 layout.css
     */
    setPageWidth() {
        let clientWd = this.VIEWPORT_WIDTH,
            platform = this.platform();

        //设置开发模式(移动端MB,电脑端PC)
        if(platform.mobile) {
            this.#docDm.className = 'model-MB';
        } else {
            clientWd = 750;
            this.#docDm.className = 'model-PC';
        }
        this.#docDm.style.fontSize = clientWd / 7.5 + 'px';
    }

    /**
     * platform - 返回一个移动终端设备的浏览器平台的对象
     * 应用场景：判断终端设备，区分 Mobile / Pc
     */
    platform() {
        const UA = navigator.userAgent;
        return {
            //是否IE内核
            trident: UA.includes('Trident'),

            //是否opera内核
            presto: UA.includes('Presto'),

            //是否苹果、谷歌内核
            webKit: UA.includes('AppleWebKit'),

            //是否火狐内核
            gecko: UA.includes('Gecko') && !UA.includes('KHTML'),

            //是否为移动终端
			mobile: /iphone|ipod|ipad|android|symbian|nokia|blackberry| rim |opera mini|opera mobi|windows ce|windows phone|up\.browser|netfront|palm-|palm os|pre\/|palmsource|avantogo|webos|hiptop|iris|kddi|kindle|lg-|lge|mot-|motorola|nintendo ds|nitro|playstation portable|samsung|sanyo|sprint|sonyericsson|symbian/i.test(UA) 
			|| /alcatel|audiovox|bird|coral|cricket|docomo|edl|huawei|htc|gt-|lava|lct|lg|lynx|mobile|lenovo|maui|micromax|mot|myphone|nec|nexian|nook|pantech|pg|polaris|ppc|sch|sec|spice|tianyu|ustarcom|utstarcom|videocon|vodafone|winwap|zte/i.test(UA),

            //是否ios终端
            ios: UA.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),

            //是否android终端或者uc浏览器
            android: UA.includes('Android') || UA.includes('Linux'),

            //是否为iPhone或者QQ HD浏览器
            iPhone: UA.includes('iPhone') || UA.includes('Mac'),

            //是否iPad
            iPad: UA.includes('iPad'),

            //是否web应该程序，没有头部与底部
            webApp: UA.includes('Safari') === false,

            //是否微信
            isWeixin: /MicroMessenger/i.test(UA)
        }
    }

    /**
     * getDate()，返回一个重新包装过的日期对象，注*该对象的属性值为字符串格式
     * D.getFullYear;      //返回4位数年份，如：2017
     * D.getMonth;         //返回2位数月份，如：03（01-12）
     * D.getDate;          //返回2位数日期，如：05（01-31）
     * D.getDay;           //返回2位数星期，如：06（01-07）
     * D.getHours;         //返回2位数小时，如：07（01-24）
     * D.getMinutes;       //返回2位数分钟，如：59（01-59）
     * D.getSeconds;       //返回2位数秒数，如：59（01-59）
	 * getFullDateTime;    //返回常规时间格式，如：2017-03-22 16:21:56
     * D.getFullDateCn;    //返回带中文单位的全字段时间字符串，如：2017年03月22日 16:21:56
     * D.getFullDateEn;    //返回英文格式的全字段时间字符串，如：03/22/2017 16:23:10
     * D.getNowTime;       //返回用':'隔开的时分秒时间字符串，如：16:24:18
     * D.getTime;          //返回当前时间的毫秒数，如：1491036906479
     * D.YMD;              //返回用'-'隔开的时分秒时间字符串，如：16-24-18
     * D.MDY;              //返回用'/'隔开的'月/日/年'的年月日时间字符串，如：03/22/2017
     */
    getDate(T) {
        let D = T ? new Date(T) : new Date(),
			getFullYear = `${D.getFullYear()}`,
            getMonth = `${D.getMonth() + 1}`,
            getDate = `${D.getDate()}`,
            getDay = `${D.getDay()}`,
            getHours = `${D.getHours()}`,
            getMinutes = `${D.getMinutes()}`,
            getSeconds = `${D.getSeconds()}`,
            getTime = `${D.getTime()}`,
            initTime = numStr => +numStr < 10 ? `0${numStr}` : numStr;

        if(getMonth.length == 1) getMonth = initTime(getMonth);
        if(getDate.length == 1) getDate = initTime(getDate);
        if(getHours.length == 1) getHours = initTime(getHours);
        if(getMinutes.length == 1) getMinutes = initTime(getMinutes);
        if(getSeconds.length == 1) getSeconds = initTime(getSeconds);

        return {
            getFullYear,
            getMonth,
            getDate,
            getDay,
            getHours,
            getMinutes,
            getSeconds,
			getFullDateTime: `${getFullYear}-${getMonth}-${getDate} ${getHours}:${getMinutes}:${getSeconds}`,
            getFullDateCn: `${getFullYear}年${getMonth}月${getDate}日 ${getHours}:${getMinutes}:${getSeconds}`,
            getFullDateEn: `${getMonth}/${getDate}/${getFullYear} ${getHours}:${getMinutes}:${getSeconds}`,
            getNowTime: `${getHours}:${getMinutes}:${getSeconds}`,
            getTime,
			getUTC: Date.UTC(D.getUTCFullYear(), D.getUTCMonth(), D.getUTCDate(), D.getUTCHours(), D.getUTCMinutes(), D.getUTCSeconds(), D.getUTCMilliseconds()),
            YMD: `${getFullYear}-${getMonth}-${getDate}`,
            MDY: `${getMonth}/${getDate}/${getFullYear}`
        };
    }

    /**
     * countdown - 倒计时，没有返回值，
     * 应用场景：判断终端设备，区分 Mobile / Pc
     */
    countdown(sendCodeBtnId, sendCodeTime, duringTimes = 60, userInfo) {
        let doc = this.#doc,
			sendCodeBtnElement = null, 
            duringTimesCopy = duringTimes;

		// 取消已经存在的定时器
		if(this.#setTimeoutTimer) clearTimeout(this.#setTimeoutTimer);

        //防止刷新后，再次调用时，第一个数字闪现上一次刷新时的数字
        this.#countdown = duringTimesCopy;

        if(sendCodeBtnId.indexOf('#') !== 0) {
            sendCodeBtnElement = doc.querySelector(`#${sendCodeBtnId}`);
        } else {
            sendCodeBtnElement = doc.querySelector(`${sendCodeBtnId}`);
        }

        const repeatCountdown = () => {
            if(this.#countdown <= 0) {
                //倒计时结束清除缓存
                this.deleteStorage({storageNames: ['UTILS_COUNTDOWN', 'UTILS_CURRENT_TIME', userInfo], storageType: localStorage});
                
                sendCodeBtnElement.textContent = '获取验证码';
                this.#countdown = duringTimesCopy;
                // 清除定时器
                clearTimeout(this.#setTimeoutTimer);
            } else {
                sendCodeBtnElement.textContent = `倒计时 ${this.#countdown} 秒`
                this.#countdown--;
                this.#setTimeoutTimer = setTimeout(() => {
                    const date = new Date(), currentTime = date.getTime();
                    //sendCodeTime    点击获取验证码的时间戳
                    //currentTime     当前时间戳
                    //this.#countdown 倒计时剩余时间/秒
                    this.#countdown = Math.ceil(duringTimes - (currentTime - sendCodeTime)/1000);
                    Utils.setStorage({
                        storageName: 'UTILS_COUNTDOWN',
                        storageValue: this.#countdown,
                        storageType: 'localStorage'
                    });
                    Utils.setStorage({
                        storageName: 'UTILS_CURRENT_TIME',
                        storageValue: currentTime,
                        storageType: 'localStorage'
                    });
                    repeatCountdown(sendCodeBtnId, sendCodeTime, duringTimes);
                }, 1000);
            }
        }
        repeatCountdown();
    }

	/**
	 * 保持倒计时，直到结束
	 */ 
    keepCountdown(sendCodeBtnId) {
        //倒计时时间
        const countdownTime = Utils.getStorage({
            storageName: 'UTILS_COUNTDOWN',
            storageType: 'localStorage'
        }) || this.#countdown

        //初始化执行倒计时
        if(0 < countdownTime && countdownTime < this.#countdown) {
            //执行倒计时
            const currentTime = Utils.getDate().getTime,
                newTimes = Utils.getStorage({
                    storageName: 'UTILS_CURRENT_TIME',
                    storageType: 'localStorage'
                }),
                nowTiems = Math.ceil(countdownTime - (currentTime - newTimes) / 1000);

            Utils.countdown(sendCodeBtnId, currentTime, nowTiems);
        }
    }

	/**
	 * 登录倒计时
	 */
	loginCountdown(validTimeBegin, duringTimes = 1800) {
		this.#loginCountdownTime = duringTimes;
		if(this.#setTimeoutTimer) clearTimeout(this.#setTimeoutTimer);

		const repeatLoginCountdown = () => {
			if(this.#loginCountdownTime <= 0) {
				// 倒计时结束，清除缓存
				this.deleteStorage({storageNames: ['LOGIN_COUNTDOWN', 'LOGIN_CURRENT_TIME'], storageType: 'localStorage'});

				// 清除定时器
				clearTimeout(this.#setTimeoutTimer);
			} else {
				this.#loginCountdownTime--;
				this.#setTimeoutTimer = setTimeout(() => {
					const date = new Date(), currentTime = date.getTime();
					// validTimeBegin				开始倒计时的时间戳
					// currentTime					当前时间戳
					// this.#loginCountdownTime		倒计时剩余时间/秒
					this.#loginCountdownTime = Math.ceil(duringTimes - (currentTime - validTimeBegin) / 1000);
					Utils.setStorage({
						storageName: 'LOGIN_COUNTDOWN',
						storageValue: this.#loginCountdownTime,
						storageType: 'localStorage'
					});
					Utils.setStorage({
						storageName: 'LOGIN_CURRENT_TIME',
						storageValue: currentTime,
						storageType: 'localStorage'
					});
					repeatLoginCountdown();
				}, 1000);
			}
		}
		repeatLoginCountdown();
	}

    /**
     * cookie - 设置(有key)/读取(无key) cookie
     * @param {String} [name]
     * @param {String} [value] [可选]
     * @param {Object} [options] [可选]
     * cookie('name', 'key', {
     *   expires: 1 // 过期时间，一天后
     * })
     */
    cookie(name, value, options = {}) {
        if(name == undefined || name === '') return;
        let docCookie = this.#doc.cookie;

        // 如果 value 没有传值, 就去获取 cookie
        if(value == undefined || value === '') {
            let cookieValue = null;
            let cookies = '';
            if(docCookie && docCookie != '') {
                cookies = docCookie.split(';');
                for (let i = 0; i < cookies.length; i++) {
                    let cookie = cookies[i].trim();
                    // 该 cookie 字符串是否以所需的名称开头
                    if(cookie.substring(0, name.length + 1) == (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }
            return cookieValue;
        }
        // value 有值，就进行 cookie 设置
        let expires = '';
        let date;
        let path = '';
        let domain = '';
        let secure = '';
        let getDate = this.getDate();
        if(options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
            if(typeof options.expires == 'number') {
                date = new Date();
                date.setTime(+getDate.getTime + (options.expires * 24 * 60 * 60 * 1000));
            } else {
                date = options.expires;
            }
            expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
        }
        path = options.path ? '; path=' + options.path : '';
        domain = options.domain ? '; domain=' + options.domain : '';
        secure = options.secure ? '; secure' : '';
        this.#doc.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
    }

    /**
     * deleteCookie - 删除cookie
     * @param {String} [key]
     */
    deleteCookie(key) {
        let date = new Date();
        date.setTime(date.getTime() - 1);
        let delValue = this.getCookie(key);
        if(!!delValue) {
            this.#doc.cookie = key + '=' + delValue + ';expires=' + date.toUTCString();
        }
    }

    /**
     * getCookie - 读取cookie
     * @param {String} [name] cookie 名称
     */
    getCookie(name) {
        var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
        if(arr = this.#doc.cookie.match(reg))
            return unescape(arr[2]);
        else
            return null;
    }

    /**
     * debounce - 防抖函数
     * 每次触发回调函数都会产生一个全新的 setTimeout 延迟事件，如果在设定的时间间隔内触发下一次回调，则会把上一次未执行的延迟事件取消掉。
     * @param {Function} [fn]
     * @param {Number} [delay]
     * 
     * 应用场景：监听input输入
     * input.oninput = debounce(function () {
     *      console.log(this.value);
     * }, 500);
     */
    debounce(fn, delay = 300) {
        let timer = null;
        return function() { // 该函数将被绑定在目标对象上 (input)，里面的 this 指代目标对象
            if(timer !== null) clearTimeout(timer);
            timer = setTimeout(() => {
                // 箭头函数没有自己的 this, 其内部的 this 指向所处作用域的 this
                // 此处的 this 指向被绑定的目标对象
                fn.call(this);

                // 执行完回调函数后，及时释放 timer 变量
                timer = null;
            }, delay);
        }
    }

    /**
     * throttle - 节流函数
     * 在第一次回调函数触发后，会产生一个 setTimeout 延迟回调事件，必须等到过了设定的时间间隔后，监听才能有效产生下一个延迟回调事件。
     * @param {Function} [fn]
     * @param {Number} [delay]
     * 
     * 应用场景：监听滚动条
     * window.onscroll = throttle(function () {
     *      console.log(1213);
     * }, 500);
     */
    throttle(fn, delay = 300) {
        let flag = true;
        return function() {
            if(flag) {
                setTimeout(() => {
                    fn.call(this);
                    flag = true;
                }, delay);
            }
            flag = null;
        }
    }

    /**
     * imgLoadingDelayed - 图片延迟加载
     * @param {String}      [imgClassName]
     * 
     * config 配置对象里传递需要延迟加载的图片样式名称，和一个可选的事件回调函数，以及设置延迟时间的毫秒数
     * imgDelayedLoading({
	 * 		imgClassName: '.wait-load-img',
	 * 		callback: null
	 * });
     */
    imgLoadingDelayed({
		imgClassName = '.wait-load-img',
		callback = null
	}) {
        if(imgClassName === 'wait-load-img') 
			imgClassName = `.${imgClassName}`;
		else
			throwErrorFn('Utils.imgLoadingDelayed', 'imgClassName', 'invalid, not exist');

        const imgs = this.#doc.querySelectorAll(imgClassName);
        const observer = new IntersectionObserver(entries => {
            entries.forEach(entry => {
                //滚动到了图片区域（图片进入视口）
                if(entry.isIntersecting) {
                    //找到本次回调的目标图片
                    const img = entry.target;
                    img.setAttribute('src', img.getAttribute('data-src'));
                    img.classList.add('active');

					// 如果配置了回调函数，则执行回调函数
					if(typeof callback === 'function') callback(img)
                    observer.unobserve(img);
                }
            });
        });

        imgs.forEach(item => observer.observe(item));
    }

    /**
     * windowScroll - 页面滚动监听
     * @param {Object}      [config] 参数对象
     * @param {Function}    [config.fn] 回调方法
     * @param {Number}      [config.delay] 节流时间
     * 
     * config 配置对象里传递一个事件处理回调函数和节流时间
     * this.$windowScroll({
     *      fn: callback,
     *      delay: 500
     *  });
     */
    windowScroll({
        fn = () => console.log('Window listener success，& the function fn is default.'),
        delay = 300,
		cancelFlag = false
    } = {}) {
        // this.#win.onscroll = this.throttle(fn, delay);
		const scrollFn = this.throttle(fn, delay);
		this.#win.addEventListener('scroll', cancelFlag ? () => {} : scrollFn);
		return scrollFn;
    }

	cancelWindowScroll(scrollFn) {
		this.#win.removeEventListener('scroll', scrollFn);
	}

    /**
     * setStorage - 设置本地缓存
     * @param {Object} [config]
     * @param {String} [config.storageType='sessionStorage'] [localStorage/sessionStorage] 默认为 sessionStorage
     * @param {String} [config.storageName]
     * @param {Object | String | Number | Boolean} [config.storageValue] 
     * 
     * config 配置对象里传递一个存储类型，一个存储名称，以及一个存储内容
     * setStorage({
     *      storageName: 'name', 
     *      storageValue: 'value', 
     *      storageType: 'localStorage',
     * })
     */
    setStorage({
		storageName = '',
		storageValue = '',
		storageType = 'sessionStorage'
	} = {}) {
        if(storageName === '' || storageName === undefined) throwErrorFn('Utils.setStorage', 'storageName', 'null');
		if(storageType !== 'sessionStorage' && storageType !== 'localStorage') throwErrorFn('Utils.setStorage', 'storageType');
		if(storageValue === '' || storageValue === undefined) throwErrorFn('Utils.setStorage', 'storageValue', 'null');
        if(storageType === 'sessionStorage') {
			typeof storageValue === 'object'
				? sessionStorage.setItem(storageName, JSON.stringify(storageValue))
				: sessionStorage.setItem(storageName, storageValue)
        } else {
			typeof storageValue === 'object'
				? localStorage.setItem(storageName, JSON.stringify(storageValue))
				: localStorage.setItem(storageName, storageValue)
        }
    }

    /**
     * getStorage - 获取本地存储
     * @param {Object}  [config]
     * @param {String}  [config.storageType='sessionStorage'] [localStorage/sessionStorage] 默认为 sessionStorage
     * @param {String}  [config.storageName] 
     * @param {Boolean} [config.deletedFlag] [可选]，默认为 false 不删除
     * 
     * config 配置对象里传递一个存储类型，一个存储名称，以及一个获取数据后是否删除该本地存储的标志
     * getStorage({
     *      storageType: 'localStorage', 
     *      storageName: 'name', 
     *      deletedFlag: false, 
     * })
     */
    getStorage({
		storageName = '',
		storageType = 'sessionStorage',
		deletedFlag = false
	} = {}) {
		if(storageName === '' || storageName === undefined) throwErrorFn('Utils.getStorage', 'storageName', 'null');
		if(storageType !== 'sessionStorage' && storageType !== 'localStorage') throwErrorFn('Utils.getStorage', 'storageType');

        let res = null;
        if(storageType === 'sessionStorage') {
            try {
                // 如果值是 string 类型，在 JSON.parse() 方法中会报错， 需要 try catch
                res = JSON.parse(sessionStorage.getItem(storageName));
            } catch (e) {
                res = sessionStorage.getItem(storageName);
            }
        } else {
            try {
                res = JSON.parse(localStorage.getItem(storageName));
            } catch (e) {
                res = localStorage.getItem(storageName);
            }
        }

        deletedFlag && this.deleteStorage({storageNames: storageName, storageType});
        return res;
    }

    /**
     * deleteStorage - 删除本地存储, 支持一次删除多个缓存
	 * @param {Object}  [config]
     * @param {String | Array}  [config.storageNames] 删除多个存储用数组，删除单个存储，支持字符串
     * @param {String}  [config.storageType='sessionStorage'] [localStorage/sessionStorage] 默认为 sessionStorage
     */
    deleteStorage({
		storageNames = '',
		storageType = 'sessionStorage'
	} = {}) {
		const sessionStorageNameList = Object.keys(sessionStorage);
		const localStorageNameList = Object.keys(localStorage);
		const fn = (storageNames, storageType) => {
			if(storageNames === '' || storageNames === undefined) throwErrorFn('Utils.deleteStorage', 'storageNames');
			if((storageType === 'sessionStorage' && !sessionStorageNameList.includes(storageNames)) || (storageType === 'localStorage' && !localStorageNameList.includes(storageNames))) throwErrorFn('Utils.deleteStorage', storageNames, 'invalid, not exist');
		}

		if(storageType !== 'sessionStorage' && storageType !== 'localStorage') throwErrorFn('Utils.deleteStorage', 'storageType');
        if(Array.isArray(storageNames)) {
			if(storageNames.length === 0) throwErrorFn('Utils.deleteStorage', 'storageNames');
            for (let i = 0; i < storageNames.length; i++) {
				fn(storageNames[i], storageType);
                storageType === 'sessionStorage' && sessionStorage.removeItem(storageNames[i]);
                storageType === 'localStorage' && localStorage.removeItem(storageNames[i]);
            }
        }

        if(typeof storageNames === 'string') {
			fn(storageNames, storageType);
            storageType === 'sessionStorage' && sessionStorage.removeItem(storageNames);
            storageType === 'localStorage' && localStorage.removeItem(storageNames);
        }
    }

    /**
     * alertMsg()，短报文弹窗提示框
     * @param {Object}  [config]
     * @param {String}  [config.message]
     * @param {String}  [config.messageType] [可选[success/error/warning/loading]]
     * @param {String}  [config.thisElementId] [可选]
     * @param {Boolean} [config.clearFlag=true] [可选] 默认 true, 可手动单击取消显示
     * 
     * config 配置对象里传递一个消息内容，一个消息类型[可选]，触发该提示动作的元素的 id (会自动添加 'active' 样式名)，以及一个
     * alertMsg({   
     *      message: 'message', 
     *      messageType: 'success', 
     *      thisElementId: 'id', 
     *      clearFlag: false, 
     *  })
     */
    alertMsg({
		message = null,
		messageType = null,
		thisElementId = null,
		clearFlag = true
	} = {}) {
        let thisElement = '',
            //icon图标样式
            iconState = '',

            //icon html标签
            iconHtml = '',
            resHtml = '',
            textColor = '',
            maskElement = null;

        if(thisElementId) {
            thisElementId = (thisElementId.indexOf('#') === 0) 
                ? thisElementId 
                : `#${thisElementId}`;

            thisElement =document.querySelector(thisElementId);
        }

        //如果消息内容为空或页面存在信息提示框，终止执行函数
        if(!message) return;
        if(document.querySelector('#mask')) return;

        //判断触发器是否已经被触发，防止重复操作
        if(thisElement) {
            if(thisElement.classList.contains('active')) return;
            thisElement.classList.add('active');
        }

        if(messageType) {
            //警告
            if(messageType == 'warning') {
                iconState = 'icon-tanhao text-warning';
                textColor = ' text-warning';
            }

            //成功
            if(messageType == 'success') {
                iconState = 'icon-zhengque-copy text-success';
                textColor = ' text-success';
            }

            //失败
            if(messageType == 'error') {
                iconState = 'icon-guanbi-copy text-danger';
                textColor = ' text-danger';
            }

            //等候加载
            if(messageType == 'loading') {
                iconState = 'icon-loading loading text-gray';
                textColor = ' text-gray';
            }
            iconHtml = `<i class="${iconState} iconfont"></i>`;
        }

        resHtml = `<div id="mask" class="mask">
            ${iconHtml}
            <span class="mask-txt word-break ${textColor}">
                ${message}
            </span>
        </div>`;

        document.querySelector('body').insertAdjacentHTML('beforeend', resHtml);
        maskElement = document.querySelector('#mask');
        middleText.call(this, '#mask', thisElement, clearFlag);
        setTimeout(function () {
            if(!maskElement.classList.contains('active')) {
                maskElement.classList.add('active');
            }
        }, 80);

        //绝对定位垂直居中
        function middleText(id, thisElement, clearFlag) {
            let el = document.querySelector(id), 
                iw = messageType ? 38 : 0,
                h = el.offsetHeight, 
                w = document.querySelector('.mask .mask-txt').offsetWidth;

            const platform = this.platform();

            if(platform.mobile) {
                h += 88;
            }

            el.style.width = (+w + 120 + iw)/100 + 'rem';
            el.style.height = h/100 + 'rem';
            el.style.marginTop = -h/(2 * 100) + 'rem';
            el.style.marginLeft = -(+w + 120 + iw)/(2 * 100) + 'rem';

            //提示框会自动消失和可手动点击关闭
            if(clearFlag) {
                //手动点击关闭
                clickDisappear(thisElement);

                //3秒后自动关闭
                setTimeout(function () {
                    disappear(thisElement);
                }, 3000);
            }
        }

        //手动关闭提示框
        function clickDisappear(thisElement) {
            maskElement.addEventListener('click', function(){
                clearAlertMsg();
                if(thisElement && thisElement.classList.contains('active')) {
                    thisElement.classList.remove('active');
                }
            });
        }

        //3秒后自动关闭提示框
        function disappear(thisElement) {
            clearAlertMsg();
            if(thisElement && thisElement.classList.contains('active')) {
                thisElement.classList.remove('active');
            }
        }

        //关闭提示框的函数
        function clearAlertMsg () {
            if(maskElement) {
                maskElement.remove();
            }
        }
    }

    /**
     * deepCopy()，对象深拷贝
     * @param {Object}   origin
     * @param {Boolean}  copyPrototypeProperKeyFlag，是否拷贝原型，默认 false
     */
    deepCopy(origin, copyPrototypeProperKeyFlag = false) {
        if(typeof origin !== 'object') return origin
        const obj = Array.isArray(origin) ? [] : {};
        let _prototype_;
        copyPrototypeProperKeyFlag && Object.getPrototypeOf(origin) && (_prototype_ = Object.getPrototypeOf(origin));
    
        const propertyNames = Object.getOwnPropertyNames(origin);
        propertyNames.forEach(item => {
            if(typeof origin[item] === 'object' && origin[item] !== null) {
                return obj[item] = this.deepCopy(origin[item], copyPrototypeProperKeyFlag);
            }
            obj[item] = origin[item];
        })
        _prototype_ && Object.setPrototypeOf(obj, _prototype_);
        return obj;
    }

    /**
     * isEmptyObject - 判断对象是否为空
     * @param {Object}   [config]
     */
    isEmptyObject(config) {
        return (Object.prototype.toString.call(config) === '[object Object]' && JSON.stringify(config) !== '{}') ? true : false;
    }

	/**
	 * 使用 canvas 检测字符串长度
	 */
	getStringTextWidth(str, font = '14px 宋体') {
		if(Object.prototype.toString.call(str) !== '[object String]') throwErrorFn('Utils.getStringTextWidth', 'str', 'invalid, not String');
		const canvas = this.#doc.createElement('canvas').getContext('2d');
		canvas.font = font;
		const r = canvas.measureText(str).width
		return r;
	}

	/**
	 * 前端分页
	 */
	dealPagination(data, pageNo = 1, pageSize = 10) {
		if(!Array.isArray(data)) throwErrorFn('Utils.dealPagination', 'data', 'invalid, not Array');
		return data.filter((item, index) => index >= (pageNo - 1) * pageSize && index < pageNo * pageSize);
	}

	/**
	 * 生成随机值
	 */
	generateMixStrFn(len = 8) {
		if(Object.prototype.toString.call(len) !== '[object Number]') throwErrorFn('Utils.generateMixStrFn', 'len', 'invalid, not Number');
		const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
		let res = '';
		for(let i = 0; i < len; i++) {
			res += chars[Math.floor(Math.random() * chars.length)];
		}
		return res;
	}

	/**
	 * 生成随机数字
	 */
	getRandomInt(min, max) {
		min = Math.ceil(min)
		max = Math.floor(max)
		return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	/**
	 * 异或加密
	 */
	xorEncrypt(str, key) {
		const res = [];
		for(let i = 0; i < str.length; i++) {
			const charCode = str.charCodeAt(i) ^ key.charCodeAt(i % key.length);
			res.push(String.fromCharCode(charCode));
		}
		return res.join('');
	}

	/**
	 * 异或加密-生成base字符串
	 */
	xorEncrypt(str, key) {
		const res = [];
		for(let i = 0; i < str.length; i++) {
			const charCode = str.charCodeAt(i) ^ key.charCodeAt(i % key.length);
			res.push(String.fromCharCode(charCode));
		}
		const encoder = new TextEncoder();
		const uint8Array = encoder.encode(res.join(''));
		return btoa(String.fromCharCode.apply(null, uint8Array));
	}
	
	/**
	 * 异或加密-base64密码解密
	 */
	xorDecrypt(encryptedBase64, key) {
		const binaryStr = atob(encryptedBase64);
		const bytes = new Uint8Array(binaryStr.length);
		for (let i = 0; i < binaryStr.length; i++) {
			bytes[i] = binaryStr.charCodeAt(i);
		}
		const decoder = new TextDecoder();
		const xorStr = decoder.decode(bytes);
		const result = [];
		for (let i = 0; i < xorStr.length; i++) {
			const keyIndex = i % key.length;
			const decryptedCharCode = xorStr.charCodeAt(i) ^ key.charCodeAt(keyIndex);
			result.push(String.fromCharCode(decryptedCharCode));
		}
		return result.join('');
	}

	/**
	 * 代码雨 ts
	 * const canvasCom = document.querySelector('canvas')!
	 * nextTick(() => codeRain(canvasCom))
	 * 需要在 nextTick方法内调用
	 */
	codeRain(canvasCom) {
		// const canvasCom = document.querySelector('canvas')!
		canvasCom.width = screen.availWidth - 20
		canvasCom.height = screen.availHeight
		let ctx = canvasCom.getContext('2d')
		const rainText = 'abcdefghijklmnopqrstuvwxyz0123456789~!#$^&*'.split('')
		const arr = Array.from({length: Math.ceil(canvasCom.width / 10)}, item => item = 0)
		
		const codeRainFn = () => {
			ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'
			ctx.fillRect(0, 0, canvasCom.width, canvasCom.height)
			ctx.fillStyle = '#0f0'
			arr.forEach((item, index) => {
				ctx.fillText(rainText[Math.floor(Math.random()*rainText.length)], index * 10, item + 10)
				arr[index] = (item > canvasCom.height || item > 15000 * Math.random()) ? 0 : item + 10
			})
		}
		setInterval(codeRainFn, 40)
	}
}

class UTILS_MATH {
    /**
     * getRandom()，获取随机数
     * @param {Object}  [config]
     * @param {Number | String}  [config.min] 最小值
     * @param {Number | String}  [config.max] 最大值
     * @param {Boolean}          [config.isInt=true] 是否整数，默认 true
     * @param {Boolean}          [config.isMaxInt=true] 是否包含最大值，默认 true
     * @param {Number | String}  [config.roundNum=2] 保留小数位, 默认保留 2 位小数（启用整数时此项无效）
     */
    getRandom({
        min, 
        max, 
        isInt = true,
        isMaxInt = true,
        roundNum = 2
    } = {}) {
		const toNumFn = num => typeof num === 'number' ? num : +num;
		const validateNumber = (val, key) => !/^-?\d+$/.test(val) && throwErrorFn('Utils_Math.getRandom', key);
		min = toNumFn(min);
		max = toNumFn(max);
		validateNumber(min, 'min');
		validateNumber(max, 'max');
        
        if(isInt) {
			let range = isMaxInt ? max - min + 1 : max - min;
            return Math.floor(Math.random() * range) + min;
        }
        return this.roundFormat((Math.random() * (max - min) + min), roundNum);
    }

    /**
     * roundFormat()，四舍五入，默认保留2位小数位
     * @param {Number | String}  [num] 需要格式化的数值
     * @param {Number}  [p=2] 保留的小数位数，默认 2位
     */
    roundFormat(num, p = 2) {
        const floatNum = parseFloat(num);
		if(isNaN(floatNum)) throwErrorFn('Utils_Math.roundFormat', 'num');
		const m = Math.pow(10, p);
		return parseFloat(`${Math.round(floatNum * m) / m.toFixed(p)}`);
    }

	/**
	 * 私有方法 - 精确计算运算方法的集合
	 * @param {Function}			[method] 运算方法（add, subtract, multiply, divide）
	 * @param {number | string}		[arg1] 第一个操作数
	 * @param {number | string}		[arg2] 第二个操作数
	 */
	#arithmeticMethods(method, arg1, arg2) {
		let n1 = 0, n2 = 0, m, m1, m2, toStrFn = val => typeof val === 'string' ? val : val.toString();
		arg1 = toStrFn(arg1);
		arg2 = toStrFn(arg2);
		if(arg1.includes('.')) n1 = arg1.split('.')[1].length;
		try {
			n2 = arg2.split('.')[1].length;
		} catch (e) {}

		if(method === 'add' || method === 'subtract') {
			arg1 = +arg1;
			arg2 = +arg2;
		} else {
			m1 = +(arg1.replace('.', ''));
			m2 = +(arg2.replace('.', ''));
		}
		switch (method) {
			case 'add':
				m = Math.pow(10, Math.max(n1, n2));
				return (arg1 * m + arg2 * m) / m;
			case 'subtract':
				m = Math.pow(10, Math.max(n1, n2));
				return (arg1 * m - arg2 * m) / m;
			case 'multiply':
				return (m1 * m2) / Math.pow(10, n1 + n2);
			case 'divide':
				return this.multiply((m1 / m2), Math.pow(10, n2 - n1));
		}
	}

    /**
     * add()，加法精确计算
     * @param {Number | String}  [arg1]
     * @param {Number | String}  [arg2]
     */
    add(arg1, arg2) {
        return this.#arithmeticMethods('add', arg1, arg2);
    }

    /**
     * subtract()，减法精确计算
     * @param {Number | String}  [arg1]
     * @param {Number | String}  [arg2]
     */
    subtract(arg1, arg2) {
        return this.#arithmeticMethods('subtract', arg1, arg2);
    }

    /**
     * multiply()，乘法精确计算
     * @param {Number | String}  [arg1]
     * @param {Number | String}  [arg2]
     */
    multiply (arg1, arg2) {
        return this.#arithmeticMethods('multiply', arg1, arg2);
    }

    /**
     * divide()，除法精确计算
     * @param {Number | String}  [arg1]
     * @param {Number | String}  [arg2]
     */
    divide(arg1, arg2) {
        return this.#arithmeticMethods('divide', arg1, arg2);
    }
}

function throwErrorFn(funName, properKey, illustrate = '') {
	throw {
		funName,
		properKey: `[${properKey}] it's ${illustrate ? illustrate : 'incorrect'}`
	}
}

const Utils = new UTILS();
const Utils_Math = new UTILS_MATH();
export { Utils, Utils_Math };